🎖️GitЯра🎖️
Commit e6abd5d77fc85c7673e4bf097bb706b687035572
Parents : fb11f0b
Author : James Rich <2199651+jamesarich@users.noreply.github.com>
Signature : Signature validation error
Date : 2026-08-20T17:09:39Z
Committer : GitHub <noreply@github.com>
Date : 2026-08-20T17:09:39Z
feat: report Android 17 memory-limiter kills via ApplicationExitInfo (#6792)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Changes
1 files changed, 53 insertions(+), 0 deletions(-)
Diff
diff --git a/androidApp/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt b/androidApp/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt
index 49c3b5ceb8..40756e9010 100644
--- a/androidApp/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt
+++ b/androidApp/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt
@@ -16,10 +16,12 @@
*/
package org.meshtastic.app
+import android.app.ActivityManager
import android.app.Application
import android.appwidget.AppWidgetProviderInfo
import android.content.Context
import android.os.Build
+import androidx.annotation.RequiresApi
import androidx.annotation.VisibleForTesting
import androidx.collection.intSetOf
import androidx.glance.appwidget.GlanceAppWidgetManager
@@ -49,6 +51,7 @@ import org.meshtastic.app.di.AndroidKoinApp
import org.meshtastic.core.common.ContextServices
import org.meshtastic.core.database.DatabaseManager
import org.meshtastic.core.repository.MeshPrefs
+import org.meshtastic.core.repository.PlatformAnalytics
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.discovery_interrupted_scan_restored
@@ -60,6 +63,14 @@ import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.seconds
import kotlin.time.toJavaDuration
+/**
+ * [android.app.ApplicationExitInfo.getDescription] reported by Android 17's per-app memory ceiling (zram-swap kill).
+ */
+private const val MEMORY_LIMITER_ANON_SWAP_REASON = "MemoryLimiter:AnonSwap"
+private const val MEMORY_LIMITER_PREFS_NAME = "memory_limiter_exit_check"
+private const val KEY_LAST_REPORTED_EXIT_TIMESTAMP = "last_reported_exit_timestamp"
+private const val MEMORY_LIMITER_SCAN_DEPTH = 5
+
/**
* The main application class for Meshtastic.
*
@@ -96,6 +107,12 @@ open class MeshUtilApplication :
// (the startup provider is removed), so getInstance() opens WorkManager's Room DB.
applicationScope.launch { scheduleMeshLogCleanup() }
+ // ApplicationExitInfo requires API 30+. A "MemoryLimiter:AnonSwap" reason means Android 17's per-app
+ // memory ceiling zram-swapped then killed the previous process — see reportMemoryLimiterExitIfPresent.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ applicationScope.launch { reportMemoryLimiterExitIfPresent() }
+ }
+
// Generate and publish widget preview for Android 15+ widget picker
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
applicationScope.launch {
@@ -174,6 +191,42 @@ open class MeshUtilApplication :
}
}
+ @RequiresApi(Build.VERSION_CODES.R)
+ private fun reportMemoryLimiterExitIfPresent() {
+ // getHistoricalProcessExitReasons() returns a persistent record, not a queue, so the timestamp
+ // watermark keeps a kill from being re-reported on every cold start. Scanning a few records
+ // (newest first) instead of just index 0 matters: a crash during the restart that follows a
+ // limiter kill slides the AnonSwap record down the list, and the newest entry alone would miss it.
+ val prefs = getSharedPreferences(MEMORY_LIMITER_PREFS_NAME, Context.MODE_PRIVATE)
+ val watermark = prefs.getLong(KEY_LAST_REPORTED_EXIT_TIMESTAMP, -1L)
+ val kill =
+ getSystemService(ActivityManager::class.java)
+ ?.getHistoricalProcessExitReasons(null, 0, MEMORY_LIMITER_SCAN_DEPTH)
+ ?.firstOrNull {
+ // contains(), not equality: OEMs may decorate the description as the rollout expands.
+ it.description?.contains(MEMORY_LIMITER_ANON_SWAP_REASON) == true && it.timestamp > watermark
+ } ?: return
+
+ // Synchronous commit() before telemetry: this path runs precisely when the OS has been killing this
+ // process, and an apply() lost to another death would re-report the same kill. Background thread.
+ if (!prefs.edit().putLong(KEY_LAST_REPORTED_EXIT_TIMESTAMP, kill.timestamp).commit()) return
+
+ // Force PlatformAnalytics construction so its Kermit log writers (Crashlytics/Datadog) are
+ // registered before we log, even though nothing else has injected it yet this early in startup.
+ get<PlatformAnalytics>()
+
+ // Unlike the recoverable, environment-driven states ExpectedCondition.kt guards against (BLE off,
+ // a denied permission), a memory-ceiling kill means this process's own footprint was too large —
+ // an actionable engineering signal, so it is deliberately reported as a non-fatal (Error), not a
+ // rate-only breadcrumb.
+ // getPss()/getRss() return 0 when the system didn't capture them; don't report that as "0KB".
+ fun formatKb(kiloBytes: Long) = if (kiloBytes > 0) "${kiloBytes}KB" else "unreported"
+ Logger.e {
+ "Previous process exit: $MEMORY_LIMITER_ANON_SWAP_REASON " +
+ "pss=${formatKb(kill.pss)} rss=${formatKb(kill.rss)}"
+ }
+ }
+
private fun scheduleMeshLogCleanup() {
val cleanupRequest =
PeriodicWorkRequestBuilder<MeshLogCleanupWorker>(repeatInterval = 1.hours.toJavaDuration()).build()
Served by rngit 1.5.2 - Generated in 0.06s